//   Copyright 2012,2013 Vaughn Vernon
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.
package com.saasovation.agilepm.application.product;

import com.saasovation.agilepm.application.ApplicationServiceLifeCycle;
import com.saasovation.agilepm.domain.model.discussion.DiscussionAvailability;
import com.saasovation.agilepm.domain.model.discussion.DiscussionDescriptor;
import com.saasovation.agilepm.domain.model.product.Product;
import com.saasovation.agilepm.domain.model.product.ProductDiscussionRequestTimedOut;
import com.saasovation.agilepm.domain.model.product.ProductId;
import com.saasovation.agilepm.domain.model.product.ProductRepository;
import com.saasovation.agilepm.domain.model.team.ProductOwner;
import com.saasovation.agilepm.domain.model.team.ProductOwnerRepository;
import com.saasovation.agilepm.domain.model.tenant.TenantId;
import com.saasovation.common.domain.model.process.ProcessId;
import com.saasovation.common.domain.model.process.TimeConstrainedProcessTracker;
import com.saasovation.common.domain.model.process.TimeConstrainedProcessTrackerRepository;

import java.util.Date;

public class ProductApplicationService {
    private final TimeConstrainedProcessTrackerRepository processTrackerRepository;
    private final ProductOwnerRepository productOwnerRepository;
    private final ProductRepository productRepository;

    public ProductApplicationService(
            ProductRepository aProductRepository,
            ProductOwnerRepository aProductOwnerRepository,
            TimeConstrainedProcessTrackerRepository aProcessTrackerRepository) {
        super();
        this.processTrackerRepository = aProcessTrackerRepository;
        this.productOwnerRepository = aProductOwnerRepository;
        this.productRepository = aProductRepository;
    }

    // TODO: additional APIs / student assignment
    public void initiateDiscussion(InitiateDiscussionCommand aCommand) {
        ApplicationServiceLifeCycle.begin();
        try {
            Product product = this.productRepository().productOfId(
                    new TenantId(aCommand.getTenantId()),
                    new ProductId(aCommand.getProductId()));
            if (product == null) {
                throw new IllegalStateException(
                        "Unknown product of tenant id: "
                                + aCommand.getTenantId()
                                + " and product id: "
                                + aCommand.getProductId());
            }
            product.initiateDiscussion(new DiscussionDescriptor(aCommand.getDiscussionId()));
            this.productRepository().save(product);
            ProcessId processId = ProcessId.existingProcessId(product.discussionInitiationId());
            TimeConstrainedProcessTracker tracker = this.processTrackerRepository()
                    .trackerOfProcessId(aCommand.getTenantId(), processId);
            tracker.completed();
            this.processTrackerRepository().save(tracker);
            ApplicationServiceLifeCycle.success();
        } catch (RuntimeException e) {
            ApplicationServiceLifeCycle.fail(e);
        }
    }

    public String newProduct(NewProductCommand aCommand) {
        return this.newProductWith(
                aCommand.getTenantId(),
                aCommand.getProductOwnerId(),
                aCommand.getName(),
                aCommand.getDescription(),
                DiscussionAvailability.NOT_REQUESTED);
    }

    public String newProductWithDiscussion(NewProductCommand aCommand) {
        return this.newProductWith(
                aCommand.getTenantId(),
                aCommand.getProductOwnerId(),
                aCommand.getName(),
                aCommand.getDescription(),
                this.requestDiscussionIfAvailable());
    }

    public void requestProductDiscussion(RequestProductDiscussionCommand aCommand) {
        Product product = this.productRepository().productOfId(
                new TenantId(aCommand.getTenantId()),
                new ProductId(aCommand.getProductId()));
        if (product == null) {
            throw new IllegalStateException(
                    "Unknown product of tenant id: "
                            + aCommand.getTenantId()
                            + " and product id: "
                            + aCommand.getProductId());
        }
        this.requestProductDiscussionFor(product);
    }

    public void retryProductDiscussionRequest(RetryProductDiscussionRequestCommand aCommand) {
        ProcessId processId = ProcessId.existingProcessId(aCommand.getProcessId());
        TenantId tenantId = new TenantId(aCommand.getTenantId());
        Product product = this.productRepository().productOfDiscussionInitiationId(
                tenantId,
                processId.id());
        if (product == null) {
            throw new IllegalStateException(
                    "Unknown product of tenant id: "
                            + aCommand.getTenantId()
                            + " and discussion initiation id: "
                            + processId.id());
        }
        this.requestProductDiscussionFor(product);
    }

    public void startDiscussionInitiation(StartDiscussionInitiationCommand aCommand) {
        ApplicationServiceLifeCycle.begin();
        try {
            Product product = this.productRepository().productOfId(
                    new TenantId(aCommand.getTenantId()),
                    new ProductId(aCommand.getProductId()));
            if (product == null) {
                throw new IllegalStateException(
                        "Unknown product of tenant id: "
                                + aCommand.getTenantId()
                                + " and product id: "
                                + aCommand.getProductId());
            }
            TimeConstrainedProcessTracker tracker = processTrackerOfProduct(product);
            this.processTrackerRepository().save(tracker);
            product.startDiscussionInitiation(tracker.processId().id());
            this.productRepository().save(product);
            ApplicationServiceLifeCycle.success();
        } catch (RuntimeException e) {
            ApplicationServiceLifeCycle.fail(e);
        }
    }

    public void timeOutProductDiscussionRequest(TimeOutProductDiscussionRequestCommand aCommand) {
        ApplicationServiceLifeCycle.begin();
        try {
            ProcessId processId = ProcessId.existingProcessId(aCommand.getProcessId());
            TenantId tenantId = new TenantId(aCommand.getTenantId());
            Product product = this.productRepository().productOfDiscussionInitiationId(
                    tenantId,
                    processId.id());
            this.sendEmailForTimedOutProcess(product);
            product.failDiscussionInitiation();
            this.productRepository().save(product);
            ApplicationServiceLifeCycle.success();
        } catch (RuntimeException e) {
            ApplicationServiceLifeCycle.fail(e);
        }
    }

    private void sendEmailForTimedOutProcess(Product aProduct) {
        // TODO: Implement
    }

    private String newProductWith(
            String aTenantId,
            String aProductOwnerId,
            String aName,
            String aDescription,
            DiscussionAvailability aDiscussionAvailability) {
        TenantId tenantId = new TenantId(aTenantId);
        ProductId productId = null;
        ApplicationServiceLifeCycle.begin();
        try {
            productId = this.productRepository().nextIdentity();
            ProductOwner productOwner = this.productOwnerRepository()
                    .productOwnerOfIdentity(tenantId, aProductOwnerId);
            Product product = new Product(
                    tenantId,
                    productId,
                    productOwner.productOwnerId(),
                    aName,
                    aDescription,
                    aDiscussionAvailability);
            this.productRepository().save(product);
            ApplicationServiceLifeCycle.success();
        } catch (RuntimeException e) {
            ApplicationServiceLifeCycle.fail(e);
        }
        assert productId != null;
        return productId.id();
    }

    private DiscussionAvailability requestDiscussionIfAvailable() {
        DiscussionAvailability availability = DiscussionAvailability.ADD_ON_NOT_ENABLED;
        boolean enabled = true; // TODO: determine add-on enabled
        if (enabled) {
            availability = DiscussionAvailability.REQUESTED;
        }
        return availability;
    }

    private TimeConstrainedProcessTrackerRepository processTrackerRepository() {
        return this.processTrackerRepository;
    }

    private ProductOwnerRepository productOwnerRepository() {
        return this.productOwnerRepository;
    }

    private ProductRepository productRepository() {
        return this.productRepository;
    }

    private void requestProductDiscussionFor(Product aProduct) {
        ApplicationServiceLifeCycle.begin();
        try {
            aProduct.requestDiscussion(this.requestDiscussionIfAvailable());
            this.productRepository().save(aProduct);
            ApplicationServiceLifeCycle.success();
        } catch (RuntimeException e) {
            ApplicationServiceLifeCycle.fail(e);
        }
    }

    private TimeConstrainedProcessTracker processTrackerOfProduct(Product aProduct) {
        TimeConstrainedProcessTracker tracker;
        if (aProduct.discussionInitiationId() != null) {
            ProcessId processId = ProcessId.existingProcessId(aProduct.discussionInitiationId());
            tracker = this.processTrackerRepository()
                    .trackerOfProcessId(aProduct.tenantId().id(), processId);
        } else {
            String timedOutEventName = ProductDiscussionRequestTimedOut.class.getName();
            tracker = new TimeConstrainedProcessTracker(
                    aProduct.tenantId().id(),
                    ProcessId.newProcessId(),
                    "Create discussion for product: "
                            + aProduct.name(),
                    new Date(),
                    5L * 60L * 1000L, // retries every 5 minutes
                    3, // 3 total retries
                    timedOutEventName);
        }
        return tracker;
    }
}
